import time  # 导入 time 模块，用于计算程序运行时间。
import os  # 导入 os 模块，用于处理文件和目录。
import cv2  # 导入 cv2 模块，用于处理图像。
import numpy as np  # 导入 numpy 模块，用于处理数组。
from imutils import paths  # 导入imutils中的paths工具,用于获取文件路径
import pickle  # 导入 pickle 模块，用于读取和保存数据。
from sklearn.model_selection import train_test_split  # 导入 train_test_split 函数，用于将数据集分割为训练集和测试集。
from sklearn.linear_model import LogisticRegression  # 导入 LogisticRegression 类，用于构建逻辑回归模型。
from sklearn.ensemble import RandomForestClassifier, VotingClassifier, BaggingClassifier, AdaBoostClassifier
from sklearn.svm import SVC  # 导入 SVC 类，用于构建支持向量机模型。
from sklearn.tree import DecisionTreeClassifier  # 导入 DecisionTreeClassifier 类，用于构建决策树模型。
from xgboost import XGBClassifier  # 导入 XGBClassfier 类，用于构建 XGBoost 模型。
from sklearn.ensemble import StackingClassifier  # 导入 StackingClassifier 类，用于构建集成模型。
from tabulate import tabulate  # 导入 tabulate 函数，用于将数据以表格的形式打印出来。
from tqdm import tqdm  # 导入 tqdm 函数，用于在循环中显示进度条。
import logging  # 导入 logging 模块，用于记录日志。
from sklearn.preprocessing import StandardScaler  # 导入 StandardScaler 类，用于数据标准化。

# 配置 logging，确保能够打印正在运行的函数名
logging.basicConfig(level=logging.INFO, format='%(asctime)s-%(levelname)s-%(message)s')


def getsize(file):  # 转换后的  MB
    return os.path.getsize(file) / (1024 * 1024)


def createXY(train_floder, dest_floder, method='flat', batch_size=64):
    x_file_path = os.path.join(dest_floder, 'X.pkl')  # 设置X文件的路径，用于保存特征数据
    y_file_path = os.path.join(dest_floder, 'y.pkl')  # 设置y文件的路径，用于保存标签数据

    if os.path.exists(x_file_path) and os.path.exists(y_file_path):
        logging.info(f'X文件大小：{getsize(x_file_path):.2f}MB')
        logging.info(f'y文件大小：{getsize(y_file_path):.2f}MB')

        # 用pickle读取X和y文件
        with open(x_file_path, 'rb') as f:
            X = pickle.load(f)
        with open(y_file_path, 'rb') as f:
            y = pickle.load(f)
        return X, y

    logging.info("读取所有图像，生成 X 和 y")

    image_paths = list(paths.list_images(train_floder))  # 获取训练文件夹中所有图像的路径
    logging.info(f"找到图像数量: {len(image_paths)}")

    if not image_paths:
        logging.error(f"没有在 {train_floder} 中找到图像")
        return [], []
    X = []  # 初始化 X 列表，用于存储特征
    y = []  # 初始化 y 列表，用于存储标签

    # 计算需要的批次数
    num_batchs = len(image_paths) // batch_size + (1 if len(image_paths) % batch_size else 0)
    for idx in tqdm(range(num_batchs), desc='读取图像'):
        batch_images = []  # 初始化存储批次图像的列表
        batch_labels = []  # 初始化存储批次标签的列表

        start = idx * batch_size  # 计算批次开始的索引
        end = min((idx + 1) * batch_size, len(image_paths))  # 计算批次结束的索引

        for i in range(start, end):  # 对于每个批次中的图像
            image_path = image_paths[i]  # 获取图像的路径
            img = cv2.imread(image_path, 0)  # 以灰度模式读取图像
            if img is None:
                logging.error(f"无法读取图像: {image_path}")
                continue
            img = cv2.resize(img, (32, 32))  # 将图像调整为32x32
            batch_images.append(img)  # 将图像数组添加到批次图像列表

            label = image_path.split(os.path.sep)[-1].split(".")[0]  # 从文件名中解析标签
            label = 1 if label == 'dog' else 0  # 如果标签是'dog'则为1,否则为0
            batch_labels.extend([label])

        batch_images = np.array(batch_images)  # 将批次图像列表转换为numpy数组
        batch_pixels = batch_images.reshape((batch_images.shape[0], -1))  # 将图像展平

        X.extend(batch_pixels)  # 将处理后的图像特征添加到X列表
        y.extend(batch_labels)  # 将标签添加到y列表
    logging.info(f"X.shape:{np.shape(X)}")  # 打印 X 的形状
    logging.info(f"y.shape:{np.shape(y)}")  # 打印 y 的形状

    with open(x_file_path, 'wb') as f:  # 打开 X 文件准备写入
        pickle.dump(X, f)  # 将 X 写入到 X 文件中
        logging.info(f"X文件大小：{getsize(x_file_path):.2f}MB")  # 打印 X 文件的大小

    with open(y_file_path, 'wb') as f:  # 打开 y 文件准备写入
        pickle.dump(y, f)
        logging.info(f"y文件大小：{getsize(y_file_path):.2f}MB")

    return X, y  # 打印 y 文件的大小


if __name__ == "__main__":

    X, y = createXY(train_floder="./train", dest_floder=".", method='flat')
    X = np.array(X).astype('float32')  # 将 X 转换为 float32 类型
    y = np.array(y)  # 将 y 转换为数组类型
    logging.info("数据加载和预处理完成")  # 打印日志信息

    # 使用stratify参数进行数据分割
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=2023)
    logging.info("数据集划分为训练集和测试集")

    scaler = StandardScaler()  # 创建标准化对象
    X_train_scaled = scaler.fit_transform(X_train)  # 对训练集进行标准化
    X_test_scaled = scaler.transform(X_test)  # 对测试集进行标准化

    # 创建逻辑回归分类器
    logistic_clf = LogisticRegression(solver='liblinear', max_iter=1000)

    # 创建随机森林分类器
    random_forest_clf = RandomForestClassifier(random_state=42)
    random_forest_clf.fit(X_train, y_train)  # 训练堆叠分类器
    with open("best_model.pkl", "wb") as f:
        pickle.dump(random_forest_clf, f)

    # 创建支持向量机分类器
    svm_clf = SVC(probability=True)

    # 创建硬投票分类器，包含逻辑回归分类器、随机森林分类器和支持向量机分类器
    voting_clf_hard = VotingClassifier(estimators=[('lr', logistic_clf), ('rf', random_forest_clf), ('svc', svm_clf)],
                                       voting='hard')

    # 创建软投票分类器，包含逻辑回归分类器、随机森林分类器和支持向量机分类器
    voting_clf_soft = VotingClassifier(estimators=[('lr', logistic_clf), ('rf', random_forest_clf), ('svc', svm_clf)]
                                       , voting='soft')

    # 创建Bagging分类器
    bag_clf = BaggingClassifier(DecisionTreeClassifier(),  # 基分类器
                                n_estimators=500,  # 基分类器数量
                                max_samples=100,  # 每个基分类器的样本数量
                                bootstrap=True,  # 放回取样
                                n_jobs=-1)  # 使用所有CPU核心

    # 创建Pasting分类器
    paste_clf = BaggingClassifier(DecisionTreeClassifier(),  # 基分类器
                                  n_estimators=500,  # 基分类器数量
                                  max_samples=100,  # 每个基分类器的样本数量
                                  bootstrap=False,  # 不放回取样
                                  n_jobs=-1)  # 使用所有CPU核心

    # 创建AdaBoost分类器
    ada_clf = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1),  # 基分类器
                                 n_estimators=200,  # 基分类器数量
                                 algorithm="SAMME.R",  # 使用SAMME.R算法
                                 learning_rate=0.5)  # 学习率为0.5

    # 创建一个梯度提升分类器
    xgb_clf = XGBClassifier(n_estimators=200,  # 基分类器数量
                            max_depth=2,  # 基分类器最大深度
                            learning_rate=0.5)  # 学习率为0.5

    # 创建一个堆叠分类器
    stacking_clf = StackingClassifier(estimators=[('lr', logistic_clf), ('rf', random_forest_clf), ('svc', svm_clf)],
                                      final_estimator=LogisticRegression())  # 最终分类器为逻辑回归分类器

    clfs = {  # 创建字典，包含所有分类器
        "logistic_regression": logistic_clf,
        "random_forest": random_forest_clf,
        "svm": svm_clf,
        "voting_clf_hard": voting_clf_hard,
        "voting_clf_soft": voting_clf_soft,
        "bagging": bag_clf,
        "pasting": paste_clf,
        "adaboost": ada_clf,
        "gradient_boosting": xgb_clf,
        "stacking": stacking_clf
    }

    results = []  # 初始化结果列表
    for name, clf in clfs.items():  # 对于字典中的每个分类器
        start_time = time.time()  # 获取当前时间
        clf.fit(X_train, y_train)  # 训练分类器
        fit_time = time.time() - start_time  # 计算训练时间
        logging.info(f"{name}模型训练完成。用时：{fit_time:2f}秒")

        start_time = time.time()  # 获取当前时间
        accuracy = clf.score(X_test, y_test)  # 计算分类器在测试集上的准确率
        score_time = time.time() - start_time  # 计算评估时间
        logging.info(f"{name}模型评估完成。用时：{score_time:2f}秒")

        best_clf_name, best_clf, best_accuracy = None, None, 0  # 初始化最佳分类器名称、分类器和准确率
        if accuracy > best_accuracy:
            best_accuracy = accuracy
            best_clf_name = name
            best_clf = clf

    logging.info(f"最佳准模型为：{best_clf_name}，准确率为：{best_accuracy:.2f}")

    with open("best_model1.pkl", "wb") as f:
        pickle.dump(best_clf, f)

        results.append([name, fit_time, score_time, accuracy])  # 将结果添加到结果列表中

    headers = ["Classifier", "Training Time (s)", "Prediction Time (s)", "accuracy"]  # 设置表头
    print(tabulate(results, headers=headers, tablefmt='simple'))  # 打印表格
